package cn.demoncat.util.ftp;

import cn.demoncat.util.conn.ConnTask;
import cn.demoncat.util.exception.BizErrorRuntimeException;
import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.ftp.config.FtpProperties;
import cn.demoncat.util.io.FileNameUtil;
import cn.demoncat.util.io.FileSizeUtil;
import cn.demoncat.util.io.ImgUtil;
import cn.demoncat.util.io.IoUtil;
import cn.demoncat.util.io.config.ImgProperties;
import cn.demoncat.util.io.constant.ImgConstant;
import cn.demoncat.util.io.entity.ImgParam;
import cn.demoncat.util.io.entity.LsFile;
import cn.demoncat.util.io.entity.LsFile.FileType;
import cn.demoncat.util.lang.IDUtil;
import cn.demoncat.util.lang.LocalDateUtil;
import cn.demoncat.util.lang.ParamEmptyUtil;
import cn.demoncat.util.lang.StringUtil;
import cn.demoncat.util.lang.constant.DateConstant;
import cn.demoncat.util.lang.constant.StatusConstant.Common;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.lang.msg.FileMsg;
import cn.demoncat.util.web.config.AppProperties;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

/**
 * FTP工具类 - 单机版
 * <pre>
 * 路径问题：
 * 所有文件路径，都是基于FTP服务端安装路径的相对路径。
 * 如果切换目录后还要进行操作，应先切换回安装路径（服务器磁盘绝对路径，而非FTP用户目录下的相对路径）。
 * 
 * 模式问题：
 * FTP会开启不同的端口来传输数据，但Linux上由于安全限制，会禁用某些端口而造成阻塞。
 * 所以需要切换ftp.mode配置，来切换被动/主动模式。
 * </pre> 
 * @author 延晓磊
 *
 * @since 2019年11月6日
 */
@Component("ftpUtil")
public class FtpUtil {

	// FTP配置
	private final FtpProperties ftpProperties;
	// 图服配置
	private final ImgProperties imgProperties;
	// 部署环境
	private final AppProperties appProperties;

	/**
	 * 创建FTP客户端
	 * 
	 * @param ftpProperties
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年11月7日
	 */
	@Autowired
	public FtpUtil(FtpProperties ftpProperties, ImgProperties imgProperties, AppProperties appProperties) {
		// 检查配置
		if (StringUtils.isAnyBlank(ftpProperties.getUrl(), ftpProperties.getUsername(), ftpProperties.getPassword(), ftpProperties.getRoot())) {
			throw new IoRuntimeException("初始化FTP客户端失败：必须配置服务端url、username、password、root");
		}
		this.ftpProperties = ftpProperties;
		this.imgProperties = imgProperties;
		this.appProperties = appProperties;
	}

	/**
	 * 获取连接 - 执行任务 - 关闭连接
	 *
	 * @param task	连接任务
	 * @return
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月4日
	 */
	public <R> R execute(ConnTask<FtpConn, R> task) {
		// 获取连接，执行操作，关闭连接
		try (FtpConn conn = new FtpConn(ftpProperties.getUrl(), ftpProperties)) {
			return task.execute(conn);
		}
	}

	/**
	 * 创建目录
	 *
	 * @param dir	目录
	 *
	 * @return 目录路径
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月28日
	 */
	public String mkdir(String dir){
		// 初始化路径
		String dirPath = initDirPath(StringUtil.checkBlank(dir));
		// 执行任务
		return execute(conn -> {
			try {
				// 切换到目录，不存在创建
				dirAndMkdir(conn.getClient(), dirPath);
				return dirPath;
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.CREATE_DIR_ERROR, e);
			}
		});
	}

	/**
	 * 文件信息转换
	 *
	 * @param file	FTP文件
	 * @return	通用文件
	 *
	 * @author 延晓磊
	 *
	 * @since 2020-9-16
	 */
	public static LsFile toLsFile(FTPFile file) {
		LsFile lf = new LsFile();
		lf.setName(file.getName());
		lf.setSize(file.getSize());
		lf.setType(file.isDirectory() ? FileType.DIR : FileType.FILE);
		// 更新时间，时区处理
		lf.setDate(LocalDateUtil.toStr(LocalDateUtil.toLocalDateTime(file.getTimestamp().getTimeInMillis() + file.getTimestamp().getTimeZone().getOffset(0))));
		lf.setUser(file.getUser());
		return lf;
	}

	/**
	 * 文件信息转换
	 *
	 * @param files	FTP文件
	 * @return [通用文件]
	 *
	 * @author 延晓磊
	 *
	 * @since 2020-9-16
	 */
	public static List<LsFile> toLsFile(FTPFile[] files) {
		List<LsFile> list = new ArrayList<>();
		if (ArrayUtils.isNotEmpty(files)) {
			for (FTPFile file : files) {
				// 排除 . 和 ..
				if (file.getName().equals(".") || file.getName().equals("..")) {
					continue;
				}
				// 转换文件信息
				list.add(toLsFile(file));
			}
		}
		return list;
	}

	/**
	 * 下载文件
	 * 
	 * @param fileName	文件全路径：不限制前缀/
	 * @param out		输出流（未关闭）
	 * 
	 * @exception BizRuntimeException		文件不存在
	 * @exception BizErrorRuntimeException	下载失败
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年5月19日
	 */
	public void download(String fileName, OutputStream out) {
		// 参数校验
		ParamEmptyUtil.checkBlank(fileName);
		ParamEmptyUtil.checkNull(out);
		// 初始化路径：前缀非/
		String filePath = initFilePath(fileName);
		// 执行任务
		execute(conn -> {
			try {
				// 下载
				if (!conn.getClient().retrieveFile(filePath, out)) {
					throw new BizRuntimeException(FileMsg.FILE_NOT_EXISTS);
				}
				return true;
			} catch (Exception e) {
				throw new BizErrorRuntimeException(FileMsg.DOWNLOAD_FILE_ERROR, e);
			}
		});
	}

	/**
	 * 删除文件 - FTP
	 *
	 * @param url	文件地址（FTP路径）
	 * @param isDir
	 *
	 * @return true删除成功；false目录非空、文件不存在
	 *
	 * @author 延晓磊
	 *
	 * @since 2020年6月17日
	 */
	public boolean delete(String url, boolean isDir) {
		ParamEmptyUtil.checkBlank(url);
		if (isDir) {
			// 删除目录
			return deleteDir(url);
		}else{
			// 删除文件
			return delete(url);
		}
	}

	/**
	 * 删除文件
	 * 
	 * @param fileName	文件全路径：不限制前缀/
	 * 
	 * @return 删除成功true，文件不存在false
	 * 
	 * @exception BizErrorRuntimeException	删除失败
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年5月19日
	 */
	public boolean delete(String fileName) {
		// 初始化路径：前缀非/
		String filePath = initFilePath(StringUtil.checkBlank(fileName));
		// 执行任务
		return execute(conn -> {
			try {
				// 删除
				return conn.getClient().deleteFile(filePath);
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.DELETE_FILE_ERROR, e);
			}
		});
	}

	/**
	 * 批量删除文件
	 * 
	 * @param fileNames	[文件全路径(不限制前缀/)]
	 * 
	 * @exception BizErrorRuntimeException	删除失败
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年5月19日
	 */
	public void delete(List<String> fileNames) {
		// 参数校验
		if (CollectionUtils.isEmpty(fileNames)) {
			return;
		}
		// 初始化路径：前缀非/
		List<String> filePaths = new ArrayList<>();
		for (String fileName : fileNames) {
			filePaths.add(initFilePath(fileName));
		}
		// 执行任务
		execute(conn -> {
			try {
				// 删除
				for (String filePath : filePaths) {
					conn.getClient().deleteFile(filePath);
				}
				return true;
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.DELETE_FILE_ERROR, e);
			}
		});
	}

	/**
	 * 删除目录
	 * 
	 * @param dirName	目录全路径：不限制前后缀/
	 * 
	 * @return 删除成功true，目录不存在/目录非空false
	 * 
	 * @exception BizErrorRuntimeException	删除失败
	 * 
	 * @author 延晓磊
	 * @return 
	 *
	 * @since 2017年5月19日
	 */
	public Boolean deleteDir(String dirName) {
		// 初始化路径：前缀非/
		String dirPath = initDirPath(StringUtil.checkBlank(dirName));
		// 执行任务
		return execute(conn -> {
			try {
				// 删除
				return conn.getClient().removeDirectory(dirPath);
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.DELETE_DIR_ERROR, e);
			}
		});
	}

	/**
	 * 查看FTP目录
	 *
	 * @param dirName	目录全路径：不限制前缀/
	 * 
	 * @return FTP目录下的目录和文件，目录不存在返回[]
	 * 
	 * @exception BizErrorRuntimeException	查看失败
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public List<LsFile> listDir(String dirName) {
		// 初始化路径：前缀非/
		String dirPath = initDirPath(StringUtil.checkBlank(dirName));
		// 执行任务
		return execute(conn -> {
			try {
				// 读取隐藏文件
				conn.getClient().setListHiddenFiles(true);
				// 遍历目录
				return toLsFile(conn.getClient().listFiles(dirPath));
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.LIST_DIR_ERROR, e);
			}
		});
	}

	/**
	 * 上传文件（指定路径）
	 *
	 * @param dirName	上传目录（不限制前后缀/：profile/module/2019/01/）
	 * @param fileName	文件名称（不限制前缀/，包含扩展名：123456.jpg）
	 * @param file		文件
	 *
	 * @return 文件路径：dirName/fileName
	 *
	 * @exception BizErrorRuntimeException 上传失败
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(String dirName, String fileName, byte[] file) {
		return upload(dirName, fileName, IoUtil.getInputStream(file));
	}

	/**
	 * 上传文件（指定路径）
	 * 
	 * @param dirName	上传目录（不限制前后缀/：profile/module/2019/01/）
	 * @param fileName	文件名称（不限制前缀/，包含扩展名：123456.jpg）
	 * @param file		文件（未关闭）
	 * 
	 * @return 文件路径：dirName/fileName
	 * 
	 * @exception BizErrorRuntimeException 上传失败
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(String dirName, String fileName, InputStream file) {
		// 初始化路径
		String dirPath = initDirPath(StringUtil.checkBlank(dirName));
		String filePath = initFilePath(StringUtil.checkBlank(fileName));
		// 执行任务
		return execute(conn -> {
			try {
				// 切换到上传目录
				dirAndMkdir(conn.getClient(), dirPath);
				// 上传(文件存在时替换)
				if (!conn.getClient().storeFile(filePath, file)) {
					throw new IOException( StringUtil.logFormat("上传文件失败：file = {}, dir =  {}, reply = {}", dirPath + filePath, conn.getClient().printWorkingDirectory(), conn.getClient().getReplyString()));
				}
				return dirPath + filePath;
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.UPLOAD_FILE_ERROR, e);
			}
		});
	}

	/**
	 * 上传文件（随机路径）
	 *
	 * @param file		文件（未关闭）
	 * @param ext		文件后缀，如 jpg
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext
	 *
	 * @exception BizErrorRuntimeException 上传失败
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(InputStream file, String ext, String module) {
		return this.upload(getMonthDirPath(module), getFileName() + FileNameUtil.setExtension(ext), file);
	}

	/**
	 * 上传文件（随机路径）
	 *
	 * @param file		文件（未关闭）
	 * @param ext		文件后缀，如 jpg
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext
	 *
	 * @exception BizErrorRuntimeException 上传失败
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(InputStream file, String ext) {
		return this.upload(file, ext, null);
	}

	/**
	 * 上传文件（随机路径）
	 *
	 * @param file		文件
	 * @param ext		文件后缀，如 jpg
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext
	 *
	 * @exception BizErrorRuntimeException 上传失败
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(byte[] file, String ext, String module) {
		return this.upload(IoUtil.getInputStream(file), ext, module);
	}

	/**
	 * 上传文件（随机路径）
	 *
	 * @param file		文件
	 * @param ext		文件后缀，如 jpg
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext
	 *
	 * @exception BizErrorRuntimeException 上传失败
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月21日
	 */
	public String upload(byte[] file, String ext) {
		return this.upload(file, ext, null);
	}

	/**
	 * WEB上传文件（随机路径）
	 *
	 * @param file		文件
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 * @param saveName	是否保存文件名：0不保存，1保存（默认，以#号拼接文件名，文件名非法时修剪），2强力保存（以#号拼接文件名，文件名非法时报错）
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext#oldFileName
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月5日
	 */
	public String upload(MultipartFile file, String module, String saveName) {
		return upload(file, null, null, module, saveName);
	}

	/**
	 * WEB上传文件（随机路径）
	 *
	 * @param file		文件
	 * @param saveName	是否保存文件名：0不保存，1保存（默认，以#号拼接文件名，文件名非法时修剪），2强力保存（以#号拼接文件名，文件名非法时报错）
	 *
	 * @return 文件路径：profile/module/2019/01/*.ext#oldFileName
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月5日
	 */
	public String upload(MultipartFile file, String saveName) {
		return upload(file, null, saveName);
	}

	/**
	 * WEB上传文件（指定路径）
	 *
	 * @param dirName	上传目录：空表示随机目录名
	 * @param fileName	上传文件名：空表示随机文件名，*表示原文件名
	 * @param file		文件
	 *
	 * @return 文件路径：dirName/fileName#oldFileName
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月5日
	 */
	public String upload(String dirName, String fileName, MultipartFile file) {
		return upload(file, dirName, fileName, null, Common.NO);
	}

	/**
	 * WEB上传文件（指定路径，原文件名）
	 *
	 * @param dirName	上传目录：空表示随机目录名
	 * @param file		文件
	 *
	 * @return 文件路径：dirName/oldFileName#oldFileName
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月5日
	 */
	public String upload(String dirName, MultipartFile file) {
		return upload(dirName, StringConstant.STAR, file);
	}

	/**
	 * WEB上传文件（指定路径）
	 *
	 * @param file		文件
	 * @param dirName	上传目录：空表示随机目录名
	 * @param fileName	上传文件名：空表示随机文件名，*表示原文件名
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 * @param saveName	是否保存文件名：0不保存，1保存（默认，以#号拼接文件名，文件名非法时修剪），2强力保存（以#号拼接文件名，文件名非法时报错）
	 *
	 * @return 文件路径：dirName/fileName#oldFileName
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年11月5日
	 */
	public String upload(MultipartFile file, String dirName, String fileName, String module, String saveName) {
		// 文件校验
		if (file == null) {
			throw new BizRuntimeException(FileMsg.UPLOAD_FILE_IS_NULL);
		}

		// 判断文件尺寸
		long fileSize = file.getSize();
		if (ImgUtil.check(file.getContentType())) {
			if(fileSize > imgProperties.getImgMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_IMG_TO_BIG + FileSizeUtil.sizeString(imgProperties.getImgMax()));
			}
		}else {
			if(fileSize > imgProperties.getFileMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_FILE_TO_BIG + FileSizeUtil.sizeString(imgProperties.getFileMax()));
			}
		}

		// 是否保存文件名
		if (StringUtils.isBlank(saveName)) {
			saveName = Common.YES;
		}
		boolean isSaveName = !Common.NO.equals(saveName);
		// 文件原名
		String oldFileName = file.getOriginalFilename();
		if (isSaveName) {
			// 校验文件名
			if (Common.isYes(saveName)) {
				oldFileName = FileNameUtil.trim(oldFileName);
			}else{
				FileNameUtil.checkForce(oldFileName);
			}
		}

		// 获取文件流
		InputStream in;
		try {
			in = file.getInputStream();
		} catch (IOException e) {
			throw new BizRuntimeException(FileMsg.UPLOAD_FILE_ERROR, e);
		}

		// 上传
		if (StringUtils.isBlank(fileName)) {
			fileName = getFileName() + FileNameUtil.setExtension(FileNameUtil.getExtension(oldFileName));
		}else{
			if (StringConstant.STAR.equals(fileName)) {
				fileName = oldFileName;
			}
		}
		String url  = this.upload(StringUtils.defaultIfBlank(dirName, getMonthDirPath(module)), fileName, in);
		// 整合文件URL
		return isSaveName ? url + StringConstant.HASH + oldFileName : url;
	}

	/**
	 * WEB上传文件（随机目录，支持图片压缩/水印/副本）
	 *
	 * <pre>
	 * 图片缩放标识：
	 * 100 		width=100(对称拉伸)
	 * 100o100	width=100|height=100(智能拉伸)
	 * 100x100	width=100&height=100(强制拉伸)
	 * L10000	尺寸大于10000时，以10000为限额计算比例并压缩
	 * Q0.1		以0.1的比例压缩
	 * </pre>
	 *
	 * @param file		文件（流未关闭）
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 * @param saveName	是否保存文件名：0不保存，1保存（默认，以#号拼接文件名，文件名非法时修剪），2强力保存（以#号拼接文件名，文件名非法时报错）
	 * @param slaves 	图片缩略尺寸标识(多个用,分隔，生成多份文件，URL为URL-slave)
	 * @param zoom		图片缩略尺寸标识(单个，对原图进行缩放)
	 * @param water		图片水印文本
	 * @param alpha		透明阀值：0表示不设置，255表示将白色设置为透明（转PNG图片）
	 *
	 * @return groupName/fileName[-slave].ext[#fileOriginalName]
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年3月22日
	 */
	public String upload(MultipartFile file, String module, String saveName, String slaves, String zoom, String water, Integer alpha) {
		// 文件校验
		if (file == null) {
			throw new ParamRuntimeException(FileMsg.UPLOAD_FILE_IS_NULL);
		}

		// 是否为图片
		boolean isImg = ImgUtil.check(file.getContentType());

		// 判断文件尺寸
		long fileSize = file.getSize();
		if (isImg) {
			if(fileSize > imgProperties.getImgMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_IMG_TO_BIG + FileSizeUtil.sizeString(imgProperties.getImgMax()));
			}
		}else {
			if(fileSize > imgProperties.getFileMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_FILE_TO_BIG + FileSizeUtil.sizeString(imgProperties.getFileMax()));
			}
		}

		// 是否保存文件名
		if (StringUtils.isBlank(saveName)) {
			saveName = Common.YES;
		}
		boolean isSaveName = !Common.NO.equals(saveName);
		// 文件原名
		String oldFileName = file.getOriginalFilename();
		if (isSaveName) {
			// 校验文件名
			if (Common.isYes(saveName)) {
				oldFileName = FileNameUtil.trim(oldFileName);
			}else{
				FileNameUtil.checkForce(oldFileName);
			}
		}
		// 文件后缀
		String ext = FileNameUtil.getExtension(oldFileName);

		// 获取文件
		byte[] bytes;
		try {
			bytes = file.getBytes();
		} catch (IOException e) {
			throw new BizRuntimeException(FileMsg.UPLOAD_FILE_ERROR, e);
		}

		// 上传
		String url;
		if (isImg) {
			// 图片上传
			url = this.uploadImg(bytes, ext, module, slaves, zoom, water, alpha);
			if (ImgParam.isAlpha(alpha) && !ImgConstant.Format.PNG.equals(ext)) {
				// 透明图片强制PNG
				oldFileName = FileNameUtil.getName(oldFileName) + ImgConstant.Suffix.PNG;
			}
		}else {
			// 文件上传
			url = upload(bytes, ext, module);
		}
		// 整合文件URL
		return isSaveName ? url + StringConstant.HASH + oldFileName : url;
	}

	/**
	 * Base64上传文件（随机目录，支持图片压缩/水印/副本）
	 *
	 * <pre>
	 * 图片缩放标识：
	 * 100 		width=100(对称拉伸)
	 * 100o100	width=100|height=100(智能拉伸)
	 * 100x100	width=100&height=100(强制拉伸)
	 * L10000	尺寸大于10000时，以10000为限额计算比例并压缩
	 * Q0.1		以0.1的比例压缩
	 * </pre>
	 *
	 * @param file			文件base64
	 * @param ext 			文件后缀名（不带.）：通过ext获取 > 通过oldFileName获取 > 通过contentType获取 > 默认png；如果是图片将导致zoom生效，所以非图片文件应保证ext/oldFileName/contentType非空
	 * @param module		模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 * @param oldFileName	文件原名
	 * @param saveName		是否保存文件名：0不保存，1保存（默认，以#号拼接文件名，文件名非法时修剪），2强力保存（以#号拼接文件名，文件名非法时报错）
	 * @param slaves 		图片缩略尺寸标识(多个用,分隔，生成多份文件，URL为URL-slave)
	 * @param zoom			图片缩略尺寸标识(单个，对原图进行缩放)
	 * @param water			图片水印文本
	 * @param alpha		透明阀值：0表示不设置，255表示将白色设置为透明（转PNG图片）
	 *
	 * @return groupName/fileName[-slave].ext[#fileOriginalName]
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年3月22日
	 */
	public String upload(String file, String ext, String module, String oldFileName, String saveName, String slaves, String zoom, String water, Integer alpha) {
		// 文件校验
		if (StringUtils.isBlank(file)) {
			throw new BizRuntimeException(FileMsg.UPLOAD_FILE_IS_NULL);
		}
		// 去除HTML文件前缀：data:image/png;base64,……
		String type = null;
		if (file.startsWith("data:") && file.contains(";base64")) {
			int i = file.indexOf(';');
			// 文件类型：image/png
			type = file.substring(5, i);
			// 文件
			file = file.substring(i+8);
		}

		// 获取文件后缀
		if (StringUtils.isBlank(ext)) {
			// 通过文件名获取
			if (StringUtils.isNotBlank(oldFileName)) {
				ext = FileNameUtil.getExtension(oldFileName);
			}else {
				// 通过contentType获取
				if (StringUtils.isNotBlank(type)) {
					ext = StringUtils.split(type,StringConstant.SEPARATOR)[1];
				} else {
					// 默认PNG
					ext = ImgConstant.Format.PNG;
				}
			}
		}

		// 是否为图片
		boolean isImg = ImgUtil.check(type == null ? ext : type);

		// 是否保存文件名
		if (StringUtils.isBlank(saveName)) {
			saveName = Common.YES;
		}
		boolean isSaveName = !Common.NO.equals(saveName);

		// 解析请求参数(Base64数据)
		byte[] bytes = Base64.decodeBase64(file);

		// 判断文件尺寸
		long fileSize = bytes.length;
		if (isImg) {
			if(fileSize > imgProperties.getImgMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_IMG_TO_BIG + FileSizeUtil.sizeString(imgProperties.getImgMax()));
			}
		}else {
			if(fileSize > imgProperties.getFileMax()){
				throw new BizRuntimeException(FileMsg.UPLOAD_FILE_TO_BIG + FileSizeUtil.sizeString(imgProperties.getFileMax()));
			}
		}

		// 上传
		String url;
		if (isImg) {
			// 图片上传
			url = this.uploadImg(bytes, ext, module, slaves, zoom, water, alpha);
		}else {
			// 文件上传
			url = upload(bytes, ext, module);
		}

		// 整合文件URL
		if (isSaveName) {
			// 校验文件名
			if (Common.isYes(saveName)) {
				oldFileName = FileNameUtil.trim(oldFileName, url);
			}else{
				FileNameUtil.checkForce(oldFileName);
			}
			if (ImgParam.isAlpha(alpha) && !ImgConstant.Format.PNG.equals(ext)) {
				// 透明图片强制PNG
				oldFileName = FileNameUtil.getName(oldFileName) + ImgConstant.Suffix.PNG;
			}
			return url + StringConstant.HASH + oldFileName;
		}else {
			return url;
		}
	}

	/**
	 * 图片上传（随机路径，支持图片处理）
	 *
	 * @param file		文件
	 * @param ext		文件扩展名(不带.)，透明图片会强制PNG
	 * @param module	模块：区分项目、模块、挂载磁盘；如果为空则使用配置值
	 * @param slaves 	图片缩略尺寸标识(多个用,分隔，生成多份文件，URL为URL-slave)
	 * @param zoom		图片缩略尺寸标识(单个，对原图进行缩放)
	 * @param water		图片水印文字
	 * @param alpha		透明阀值：0表示不设置，255表示将白色设置为透明（转PNG图片）
	 *
	 * @return module/fileName.ext
	 *
	 * @author 延晓磊
	 *
	 * @since 2020年5月24日
	 */
	public String uploadImg(byte[] file, String ext, String module, String slaves, String zoom, String water, Integer alpha) {
		// 文件校验
		if (file == null) {
			throw new BizRuntimeException(FileMsg.UPLOAD_FILE_IS_NULL);
		}
		// 文件处理
		if (ImgParam.isNeed(zoom, water, alpha)) {
			file = ImgUtil.out(file, ImgParam.build().zoom(zoom).watermark(ImgUtil.getWatermark(water)).alpha(alpha));
		}
		byte[] finalFile = file;
		// 目录
		String dirName = getMonthDirPath(module);
		// 文件名
		String fileName = getFileName();
		// 后缀名
		if (ImgParam.isAlpha(alpha)) {
			// 透明图片强制PNG
			ext = ImgConstant.Format.PNG;
		}
		String extName = FileNameUtil.setExtension(ext);
		// 执行任务
		return execute(conn -> {
			try {
				// 切换到上传目录
				dirAndMkdir(conn.getClient(), dirName);
				// 上传原图
				String fullName = fileName + extName;
				if (!conn.getClient().storeFile(fullName, IoUtil.getInputStream(finalFile))) {
					throw new IOException( StringUtil.logFormat("上传文件失败：file = {}, dir =  {}, reply = {}", dirName + fullName, conn.getClient().printWorkingDirectory(), conn.getClient().getReplyString()));
				}
				// 上传副本
				if (StringUtils.isNotBlank(slaves)) {
					ByteArrayInputStream in;
					ByteArrayOutputStream out;
					for (String slave : StringUtil.split(slaves)) {
						in = new ByteArrayInputStream(finalFile);
						out = new ByteArrayOutputStream();
						// 缩放图片
						ImgUtil.zooms(in, out, slave);
						// 上传副本：fileName-slave.ext
						fullName = fileName + StringConstant.BAR + slave + extName;
						if (!conn.getClient().storeFile(fullName, IoUtil.getInputStream(out.toByteArray()))) {
							throw new IOException( StringUtil.logFormat("上传文件失败：file = {}, dir =  {}, reply = {}", dirName + fullName, conn.getClient().printWorkingDirectory(), conn.getClient().getReplyString()));
						}
					}
				}
				return dirName + fileName + extName;
			} catch (IOException e) {
				throw new BizErrorRuntimeException(FileMsg.UPLOAD_FILE_ERROR, e);
			}
		});
	}


	/**
	 * 获取文件上传目录 - 年/月
	 *
	 * @param module	模块：业务目录
	 *
	 * @return 目录路径：部署环境/配置模块(磁盘)/项目/指定模块(业务)/年/月/ >> app.active/ftp.module/app.isolator/module/2019/01/
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年1月25日
	 */
	private String getMonthDirPath(String module) {
		return this.getDirPath(module) + LocalDate.now().format(DateConstant.MONTH_FORMAT_FILE) + StringConstant.SEPARATOR;
	}

	/**
	 * 获取文件上传目录
	 *
	 * @param module	模块：业务目录
	 *
	 * @return 目录路径：部署环境/配置模块(磁盘)/项目/指定模块(业务)/ >> app.active/ftp.module/app.isolator/module/
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年1月25日
	 */
	public String getDirPath(String module){
		StringBuilder dirPath = new StringBuilder();
		// 部署环境：区分混合管理的多环境文件
		if (Common.YES.equals(ftpProperties.getProfile())) {
			dirPath.append(appProperties.getActive()).append(StringConstant.SEPARATOR);
		}
		// 模块：区分项目、模块、挂载磁盘
		if (StringUtils.isNotBlank(ftpProperties.getModule())) {
			// 配置模块：通常为磁盘分区、项目目录
			dirPath.append(ftpProperties.getModule()).append(StringConstant.SEPARATOR);
		}
		if (StringUtils.isNotBlank(appProperties.getIsolator())) {
			// 项目目录：隔离项目数据
			dirPath.append(appProperties.getIsolator()).append(StringConstant.SEPARATOR);
		}
		if (StringUtils.isNotBlank(module)) {
			// 指定模块：隔离业务数据
			dirPath.append(module).append(StringConstant.SEPARATOR);
		}
		return dirPath.toString();
	}

	/**
	 * 随机文件上传名称
	 *
	 * @return 文件名称，不带扩展名
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年1月25日
	 */
	private String getFileName() {
		return IDUtil.getSnowflakeId();
	}

	/**
	 * 初始化目录参数
	 *
	 * @param dirPath	目录路径
	 *
	 * @return 目录路径：去除前缀/，添加后缀/
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月28日
	 */
	private String initDirPath(String dirPath) {
		// 去除前缀 /
		if (dirPath.startsWith(StringConstant.SEPARATOR)) {
			if (dirPath.length() > 1) {
				dirPath = dirPath.substring(1);
			}else {
				dirPath = StringConstant.EMPTY;
			}
		}
		// 增加后缀 /
		if (!dirPath.endsWith(StringConstant.SEPARATOR)) {
			dirPath += StringConstant.SEPARATOR;
		}
		return dirPath;
	}

	/**
	 * 初始化文件路径参数
	 *
	 * @param filePath		文件路径
	 *
	 * @return 文件路径：去除前缀/
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年5月28日
	 */
	private String initFilePath(String filePath) {
		// 去除前缀/
		if (filePath.startsWith(StringConstant.SEPARATOR)) {
			if (filePath.length() > 1) {
				filePath = filePath.substring(1);
			}else {
				filePath = StringConstant.EMPTY;
			}
		}
		return filePath;
	}

	/**
	 * 切换FTP目录，如果目录不存在则创建
	 *
	 * @param client 	FTP客户端
	 * @param dir		目录，支持多层级
	 *
	 * @throws IOException
	 *
	 * @author 延晓磊
	 *
	 *
	 * @since 2017年5月28日
	 */
	private void dirAndMkdir(FTPClient client, String dir) throws IOException {
		// 直接切换
		if (!client.changeWorkingDirectory(dir)) {
			for (String d : StringUtils.split(dir, StringConstant.SEPARATOR)) {
				// 切换目录
				if (!client.changeWorkingDirectory(d)) {
					// 切换失败时创建
					client.makeDirectory(d);
					// 创建完成后切换
					client.changeWorkingDirectory(d);
				}
			}
		}
	}
}
